////////////////////////////////////////////////////////////////////////////
//  Copyright 2009 - 2011 Zach Snow                                       //
////////////////////////////////////////////////////////////////////////////
//  This file is part of WindowsWhere.                                    //
//                                                                        //
//  WindowsWhere is free software: you can redistribute it and/or modify  //
//  it under the terms of the GNU General Public License as published by  //
//  the Free Software Foundation, either version 3 of the License, or     //
//  (at your option) any later version.                                   //
//                                                                        //
//  WindowsWhere is distributed in the hope that it will be useful,       //
//  but WITHOUT ANY WARRANTY; without even the implied warranty of        //
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         //
//  GNU General Public License for more details.                          //
//                                                                        //
//  You should have received a copy of the GNU General Public License     //
//  along with WindowsWhere.  If not, see <http://www.gnu.org/licenses/>. //
////////////////////////////////////////////////////////////////////////////
// This file was derived from ProgramForm.cs, by Chris Wilson             //
////////////////////////////////////////////////////////////////////////////
using System;
using System.Drawing;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
using System.Configuration;
using System.Text;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
using Microsoft.Win32;

namespace WindowsWhere
{
  public class ProgramForm : System.Windows.Forms.Form, ILog
  {
    #region Constants
    private static string ApplicationName = "WindowsWhere";

    /// <summary>
    /// The name of the window class that Explorer uses for its windows;
    /// this is used to make sure we only move Explorer windows around.
    /// </summary>
    private static string ExplorerWindowClass = "CabinetWClass";

    /// <summary>
    /// The window text of an Explorer window before it is fully initialized.
    /// This is used to make sure we haven't caught a window after creation but
    /// before initialization; it is still hackish though (see
    /// WindowsWhereSettings.WaitTime).
    /// </summary>
    private static string ExplorerWindowText = "Windows Explorer";
    #endregion

    private GlobalHooks m_GlobalHooks = null;
    private List<Window> m_Windows = new List<Window>();

    #region Settings
    private WindowsWhereSettings Settings
    {
      get;
      set;
    }

    /// <summary>
    /// The settings file name: "%AppData%\WindowsWhere.settings.xml".
    /// </summary>
    private string SettingsFile
    {
      get
      {
        var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
        var ww = Path.Combine(appData, "WindowsWhere");
        if (!Directory.Exists(ww))
        {
          Directory.CreateDirectory(ww);
        }
        return Path.Combine(ww, "settings.xml");
      }
    }

    private RegistryKey m_RegistryKey =
      Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", true);

    /// <summary>
    /// Determines whether or not WindowsWhere should start with Windows;
    /// just checks for and creates/deletes the relevant registry key.
    /// </summary>
    public bool StartWithWindows
    {
      get
      {
        return ((string)m_RegistryKey.GetValue(ApplicationName, string.Empty)) != string.Empty;
      }
      set
      {
        menuFileStartWithWindows.Checked = value;
        if (value && !StartWithWindows)
        {
          m_RegistryKey.SetValue(ApplicationName, "\"" + Application.ExecutablePath.ToString() + "\"");
        }
        else if (!value && StartWithWindows)
        {
          m_RegistryKey.DeleteValue(ApplicationName);
        }
      }
    }

    /// <summary>
    /// Determines whether or not WindowsWhere should manage non-Explorer windows.
    /// </summary>
    public bool EnableForAllWindows
    {
      get
      {
        return Settings.EnableForAllWindows;
      }
      set
      {
        Settings.EnableForAllWindows = value;
        menuFileEnableForAll.Checked = value;
      }
    }

    public bool IsRunning
    {
      get
      {
        return Settings.IsRunning;
      }
      set
      {
        Settings.IsRunning = value;
        menuFileRun.Checked = value;
      }
    }
    #endregion

    private IContainer components;
    private System.Windows.Forms.GroupBox grpWindows;
    private System.Windows.Forms.ListBox listWindows;
    private MenuStrip menu;
    private ToolStripMenuItem menuFile;
    private ToolStripMenuItem menuHelp;
    private ToolStripMenuItem menuFileRun;
    private ToolStripSeparator toolStripSeparator1;
    private ToolStripMenuItem menuFileExit;
    private GroupBox grpLog;
    private ListBox listLog;
    private ToolStripMenuItem menuFileStartWithWindows;
    private NotifyIcon notifyIcon;
    private ToolStripMenuItem menuFileEnableForAll;
    private ToolStripMenuItem menuFileSettings;
    private ToolStripSeparator toolStripSeparator2;
    private ToolStripMenuItem menuHelpAbout;

    public ProgramForm()
    {
      InitializeComponent();

      //Needs to happen here so that IsRunning is set correctly.
      LoadSettings();

      // Create our GlobalHooks object and setup our hook events
      m_GlobalHooks = new GlobalHooks(this.Handle);
      m_GlobalHooks.Shell.WindowActivated += new GlobalHooks.WindowEventHandler(Shell_WindowActivated);
      m_GlobalHooks.Shell.WindowCreated += new GlobalHooks.WindowEventHandler(Shell_WindowCreated);
      m_GlobalHooks.Shell.WindowDestroyed += new GlobalHooks.WindowEventHandler(Shell_WindowDestroyed);

      // Menu properties.
      menuFileRun.Checked = IsRunning;
      menuFileStartWithWindows.Checked = StartWithWindows;
      menuFileEnableForAll.Checked = EnableForAllWindows;

      // Tray Icon
      notifyIcon.DoubleClick += new EventHandler(NotifyIcon_DoubleClick);
      this.Resize += new EventHandler(ProgramForm_Resize);
      this.Load += new EventHandler(ProgramForm_Load);


      //Start if we exited while running.
      if (IsRunning)
      {
        Log("starting immediately.");
        Start();
      }
      else
      {
        Log("not starting immediately.");
      }
    }

    protected override void Dispose(bool disposing)
    {
      // Make sure we remove the hooks before closing the program!
      Stop();

      if (disposing)
      {
        if (components != null)
        {
          components.Dispose();
        }
      }
      base.Dispose(disposing);
    }

    #region Windows Form Designer generated code
    /// <summary>
    /// Required method for Designer support - do not modify
    /// the contents of this method with the code editor.
    /// </summary>
    private void InitializeComponent()
    {
      this.components = new System.ComponentModel.Container();
      System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(ProgramForm));
      this.grpWindows = new System.Windows.Forms.GroupBox();
      this.listWindows = new System.Windows.Forms.ListBox();
      this.menu = new System.Windows.Forms.MenuStrip();
      this.menuFile = new System.Windows.Forms.ToolStripMenuItem();
      this.menuFileRun = new System.Windows.Forms.ToolStripMenuItem();
      this.menuFileStartWithWindows = new System.Windows.Forms.ToolStripMenuItem();
      this.menuFileEnableForAll = new System.Windows.Forms.ToolStripMenuItem();
      this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator();
      this.menuFileExit = new System.Windows.Forms.ToolStripMenuItem();
      this.menuHelp = new System.Windows.Forms.ToolStripMenuItem();
      this.menuHelpAbout = new System.Windows.Forms.ToolStripMenuItem();
      this.grpLog = new System.Windows.Forms.GroupBox();
      this.listLog = new System.Windows.Forms.ListBox();
      this.notifyIcon = new System.Windows.Forms.NotifyIcon(this.components);
      this.toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator();
      this.menuFileSettings = new System.Windows.Forms.ToolStripMenuItem();
      this.grpWindows.SuspendLayout();
      this.menu.SuspendLayout();
      this.grpLog.SuspendLayout();
      this.SuspendLayout();
      // 
      // grpWindows
      // 
      this.grpWindows.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
                  | System.Windows.Forms.AnchorStyles.Right)));
      this.grpWindows.Controls.Add(this.listWindows);
      this.grpWindows.Location = new System.Drawing.Point(12, 27);
      this.grpWindows.Name = "grpWindows";
      this.grpWindows.Size = new System.Drawing.Size(705, 297);
      this.grpWindows.TabIndex = 4;
      this.grpWindows.TabStop = false;
      this.grpWindows.Text = "Managed Windows";
      // 
      // listWindows
      // 
      this.listWindows.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
                  | System.Windows.Forms.AnchorStyles.Right)));
      this.listWindows.Location = new System.Drawing.Point(3, 16);
      this.listWindows.Name = "listWindows";
      this.listWindows.Size = new System.Drawing.Size(699, 277);
      this.listWindows.TabIndex = 3;
      // 
      // menu
      // 
      this.menu.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
            this.menuFile,
            this.menuHelp});
      this.menu.Location = new System.Drawing.Point(0, 0);
      this.menu.Name = "menu";
      this.menu.Size = new System.Drawing.Size(729, 24);
      this.menu.TabIndex = 5;
      this.menu.Text = "menuStrip1";
      // 
      // menuFile
      // 
      this.menuFile.CheckOnClick = true;
      this.menuFile.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
            this.menuFileRun,
            this.menuFileStartWithWindows,
            this.menuFileEnableForAll,
            this.toolStripSeparator1,
            this.menuFileSettings,
            this.toolStripSeparator2,
            this.menuFileExit});
      this.menuFile.Name = "menuFile";
      this.menuFile.Size = new System.Drawing.Size(37, 20);
      this.menuFile.Text = "&File";
      // 
      // menuFileRun
      // 
      this.menuFileRun.Name = "menuFileRun";
      this.menuFileRun.Size = new System.Drawing.Size(194, 22);
      this.menuFileRun.Text = "Run";
      this.menuFileRun.Click += new System.EventHandler(this.menuFileRun_Click);
      // 
      // menuFileStartWithWindows
      // 
      this.menuFileStartWithWindows.Name = "menuFileStartWithWindows";
      this.menuFileStartWithWindows.Size = new System.Drawing.Size(194, 22);
      this.menuFileStartWithWindows.Text = "Start with &Windows";
      this.menuFileStartWithWindows.Click += new System.EventHandler(this.menuFileStartWithWindows_Click);
      // 
      // menuFileEnableForAll
      // 
      this.menuFileEnableForAll.Name = "menuFileEnableForAll";
      this.menuFileEnableForAll.Size = new System.Drawing.Size(194, 22);
      this.menuFileEnableForAll.Text = "Enable for all Windows";
      this.menuFileEnableForAll.Click += new System.EventHandler(this.menuFileEnableForAll_Click);
      // 
      // toolStripSeparator1
      // 
      this.toolStripSeparator1.Name = "toolStripSeparator1";
      this.toolStripSeparator1.Size = new System.Drawing.Size(191, 6);
      // 
      // menuFileExit
      // 
      this.menuFileExit.Name = "menuFileExit";
      this.menuFileExit.Size = new System.Drawing.Size(194, 22);
      this.menuFileExit.Text = "E&xit";
      this.menuFileExit.Click += new System.EventHandler(this.menuFileExit_Click);
      // 
      // menuHelp
      // 
      this.menuHelp.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
            this.menuHelpAbout});
      this.menuHelp.Name = "menuHelp";
      this.menuHelp.Size = new System.Drawing.Size(44, 20);
      this.menuHelp.Text = "&Help";
      // 
      // menuHelpAbout
      // 
      this.menuHelpAbout.Name = "menuHelpAbout";
      this.menuHelpAbout.Size = new System.Drawing.Size(152, 22);
      this.menuHelpAbout.Text = "&About";
      this.menuHelpAbout.Click += new System.EventHandler(this.menuHelpAbout_Click);
      // 
      // grpLog
      // 
      this.grpLog.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
                  | System.Windows.Forms.AnchorStyles.Right)));
      this.grpLog.Controls.Add(this.listLog);
      this.grpLog.Location = new System.Drawing.Point(12, 329);
      this.grpLog.Name = "grpLog";
      this.grpLog.Size = new System.Drawing.Size(705, 135);
      this.grpLog.TabIndex = 6;
      this.grpLog.TabStop = false;
      this.grpLog.Text = "Log";
      // 
      // listLog
      // 
      this.listLog.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
                  | System.Windows.Forms.AnchorStyles.Right)));
      this.listLog.FormattingEnabled = true;
      this.listLog.Location = new System.Drawing.Point(3, 21);
      this.listLog.Name = "listLog";
      this.listLog.Size = new System.Drawing.Size(699, 108);
      this.listLog.TabIndex = 0;
      // 
      // notifyIcon
      // 
      this.notifyIcon.BalloonTipText = "WindowsWhere";
      this.notifyIcon.Icon = ((System.Drawing.Icon)(resources.GetObject("notifyIcon.Icon")));
      this.notifyIcon.Text = "WindowsWhere";
      this.notifyIcon.Visible = true;
      // 
      // toolStripSeparator2
      // 
      this.toolStripSeparator2.Name = "toolStripSeparator2";
      this.toolStripSeparator2.Size = new System.Drawing.Size(191, 6);
      // 
      // menuFileSettings
      // 
      this.menuFileSettings.Name = "menuFileSettings";
      this.menuFileSettings.Size = new System.Drawing.Size(194, 22);
      this.menuFileSettings.Text = "&Settings";
      this.menuFileSettings.Click += new System.EventHandler(this.menuFileSettings_Click);
      // 
      // ProgramForm
      // 
      this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
      this.ClientSize = new System.Drawing.Size(729, 467);
      this.Controls.Add(this.grpLog);
      this.Controls.Add(this.grpWindows);
      this.Controls.Add(this.menu);
      this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
      this.MainMenuStrip = this.menu;
      this.Name = "ProgramForm";
      this.Text = "WindowsWhere";
      this.grpWindows.ResumeLayout(false);
      this.menu.ResumeLayout(false);
      this.menu.PerformLayout();
      this.grpLog.ResumeLayout(false);
      this.ResumeLayout(false);
      this.PerformLayout();

    }
    #endregion

    #region Tray Icon
    private void MinimizeToTray()
    {
      if (WindowState != FormWindowState.Minimized)
      {
        WindowState = FormWindowState.Minimized;
      }
      Hide();
    }

    private void RestoreFromTray()
    {
      Show();
      WindowState = FormWindowState.Normal;
      BringToFront();
      Focus();
    }
    #endregion

    private void Start()
    {
      IsRunning = true;

      // Return a list of all open, titled, top-level windows (excluding the
      // "Program Manager" window, which Microsoft has kept as a top-level
      // window since Windows 3.0, just to be a real pain in the butt to us programmers.
      EnumWindowList windows = new EnumWindowList(true, true, true, new string[] { "Program Manager" });
      foreach (var w in windows)
      {
        AddWindow(w);
      }
      UpdateWindowList();

      // Start hooking
      Log("starting hook.");
      m_GlobalHooks.Shell.Start();
    }

    private void Stop()
    {
      //Save before setting IsRunning to false; since we might
      //call Stop() when exiting we want IsRunning to be saved
      //as what it was *before* we called stop (so if it was
      //already stopped by hand, we'll restart stopped, and if
      //it was running, we'll restart running).
      SaveSettings();
      IsRunning = false;

      // Remove our hooks
      Log("stopping hook.");
      m_GlobalHooks.Shell.Stop();

      // Iterate through all windows we've been monitoring, undoing any changes we've done to them
      if (m_Windows != null)
      {
        foreach (var w in m_Windows)
        {
          w.StopManaging();
        }
      }
      m_Windows.Clear();
      UpdateWindowList();
    }

    #region Global Hook Handlers
    void Shell_WindowActivated(IntPtr handle)
    {
      var window = new Window(handle);
      if (window.WindowClass == ExplorerWindowClass)
      {
        window.Activate();
      }
    }

    private void Shell_WindowCreated(IntPtr handle)
    {
      AddWindow(new Window(handle));
    }

    private void Shell_WindowDestroyed(IntPtr handle)
    {
      Window w = FindWindow(handle);
      if (w != null)
      {
        SaveState(w);
        RemoveWindow(handle);
      }
      UpdateWindowList();
    }
    #endregion

    #region Window Management
    private void AddWindow(Window w)
    {
      if (EnableForAllWindows && w.WindowClass != ExplorerWindowClass)
      {
        // Always wait for non-explorer windows, because there is no
        // way to know in general if an application is 'ready' or not.
        System.Threading.Thread.Sleep(Settings.WaitTime);
        m_Windows.Add(w);
        LoadState(w);
        Log("non-explorer window; allowing.");
      }
      else if (w.WindowClass == ExplorerWindowClass)
      {
        // Only wait on explorer windows that are not yet initialized.
        if (w.WindowText != ExplorerWindowText)
        {
          m_Windows.Add(w);
          LoadState(w);
        }
        else
        {
          System.Threading.Thread.Sleep(Settings.WaitTime);
          if (w.WindowText != ExplorerWindowText)
          {
            m_Windows.Add(w);
            LoadState(w);
          }
          else
          {
            // Still not initialized!
            Log("uninitialized explorer window.");
          }
        }
      }
      else
      {
        Log("non-explorer window; ignoring.");
      }
      UpdateWindowList();
    }

    private Window FindWindow(IntPtr handle)
    {
      foreach (var w in m_Windows)
      {
        if (w.Handle == handle)
          return w;
      }
      return null;
    }

    private void RemoveWindow(IntPtr handle)
    {
      for (var i = 0; i < m_Windows.Count; i++)
      {
        if (m_Windows[i].Handle == handle)
        {
          m_Windows.RemoveAt(i);
          break;
        }
      }
      return;
    }

    private void UpdateWindowList()
    {
      listWindows.Items.Clear();
      foreach (var w in m_Windows)
      {
        listWindows.Items.Add(w.WindowText);
      }
    }
    #endregion

    #region ILog Implementation
    public void Log(string s)
    {
      listLog.Items.Add("Log: " + s);
      listLog.TopIndex = listLog.Items.Count - 1;
    }

    public void Error(string s)
    {
      listLog.Items.Add("Error: " + s);
      listLog.TopIndex = listLog.Items.Count - 1;
    }
    #endregion

    #region Loading/Saving
    private void LoadState(Window w)
    {
      var s = w.WindowText;

      if (Settings.WindowSettings.ContainsKey(s))
      {
        var settings = Settings.WindowSettings[s];
        w.Rectangle = settings.Rectangle;
        Log("set position (" + settings.Rectangle.X + ", " + settings.Rectangle.Y + ")");
        Log("set size (" + settings.Rectangle.Width + ", " + settings.Rectangle.Height + ")");
        Log("loaded window settings (" + s + ")");
      }
      else
      {
        Log("no window settings (" + s + ")");
      }
    }

    private void SaveState(Window w)
    {
      var s = w.WindowText;
      try
      {
        var r = w.Rectangle;
        if (r.X == -32000 || r.Y == -32000)
        {
          Error("invalid window position (" + s + ")");
        }
        else if (Settings.WindowSettings.ContainsKey(s))
        {
          var settings = Settings.WindowSettings[s];
          settings.Rectangle = r;
          Log("updated window settings (" + s + ")");
        }
        else
        {
          Settings.WindowSettings[s] = new WindowSettings(r);
          Log("saved window settings (" + s + ")");
        }
      }
      catch (InvalidOperationException)
      {
        Log("unable to save window settings (" + s + ")");
      }
    }

    private void LoadSettings()
    {
      Settings = new WindowsWhereSettings();
      try
      {
        if (File.Exists(SettingsFile))
        {
          using (var reader = new XmlTextReader(SettingsFile))
          {
            var xml = new XmlSerializer(typeof(WindowsWhereSettings));
            Settings = (WindowsWhereSettings)xml.Deserialize(reader);
            reader.Close();
          }
          Log("loaded global settings (" + SettingsFile + ")");
        }
        else
        {
          Log("global settings file does not exist.");
        }
      }
      catch (Exception e)
      {
        Error("unable to load global settings (" + SettingsFile + ")");
        MessageBox.Show("Unable to load settings:\n  " + e.CompleteMessage(),
          "WindowsWhere - Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
      }
    }

    private void SaveSettings()
    {
      //Save window positions to settings.
      foreach (var w in m_Windows)
      {
        SaveState(w);
      }

      //Save settings.
      try
      {
        using (var writer = new XmlTextWriter(SettingsFile, System.Text.Encoding.Default))
        {
          var xml = new XmlSerializer(typeof(WindowsWhereSettings));
          xml.Serialize(writer, Settings);
          writer.Close();
        }
        Log("saved gobal settings.");
      }
      catch (Exception e)
      {
        Error("unable to save global settings (" + SettingsFile + ")");
        MessageBox.Show("Unable to save global settings:\n  " + e.CompleteMessage(),
          "WindowsWhere - Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
      }
    }
    #endregion

    #region Overrides
    protected override void WndProc(ref Message m)
    {
      //This lets the GlobalHooks class check the message queue for this
      //window to see if it's received any hook messages; need to check for
      //null because this can get called pretty much whenever.
      if (IsRunning && m_GlobalHooks != null)
      {
        m_GlobalHooks.ProcessWindowMessage(ref m);
      }
      base.WndProc(ref m);
    }

    protected override void OnClosed(EventArgs e)
    {
      SaveSettings();
      notifyIcon.Visible = false;
      base.OnClosed(e);

      Application.Exit();
    }

    protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
    {
      if (msg.Msg == Win32.WM_KEYDOWN && keyData == Keys.Escape)
      {
        MinimizeToTray();
        return true;
      }
      return base.ProcessCmdKey(ref msg, keyData);
    }

    #endregion

    #region Event Handlers
    private void menuFileRun_Click(object sender, EventArgs e)
    {
      if (!IsRunning)
        Start();
      else
        Stop();
    }

    private void menuFileExit_Click(object sender, EventArgs e)
    {
      Close();
    }

    private void menuFileStartWithWindows_Click(object sender, EventArgs e)
    {
      StartWithWindows = !StartWithWindows;
    }

    private void menuFileEnableForAll_Click(object sender, EventArgs e)
    {
      EnableForAllWindows = !EnableForAllWindows;
    }

    private void menuHelpAbout_Click(object sender, EventArgs e)
    {
      var dlg = new AboutDialog();
      dlg.ShowDialog();
    }

    private void ProgramForm_Resize(object sender, EventArgs e)
    {
      if (WindowState == FormWindowState.Minimized)
      {
        MinimizeToTray();
      }
    }

    private void ProgramForm_Load(object sender, EventArgs e)
    {
      if (StartWithWindows)
      {
        MinimizeToTray();
      }
    }

    private void NotifyIcon_DoubleClick(object sender, EventArgs e)
    {
      RestoreFromTray();
    }
    #endregion

    private void menuFileSettings_Click(object sender, EventArgs e)
    {
      var dialog = new SettingsForm();
      dialog.WaitTime = Settings.WaitTime;

      if (dialog.ShowDialog() == DialogResult.OK)
      {
        Settings.WaitTime = dialog.WaitTime;
      }
    }
  }
}
